package jdclib.io;

import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.util.LinkedList;
import java.util.Stack;

import jdclib.structs.ByteArrayBuilder;
import sun.nio.cs.StreamDecoder;

/***
 * 
 * The HybridReader class supports delimited reading of an input stream, as well as convenience methods for reading objects 
 * implementing the Parseable interface.
 *
 *@author Davis Clark
 *
 */
public class HybridReader extends Reader 
{
	private final StreamDecoder sd;

    /**
     * Creates a HybridReader that uses the default charset.
     * @param  in   An InputStream
     */
    public HybridReader(InputStream in) 
    {
		super(in);
        try 
        {
        	sd = StreamDecoder.forInputStreamReader(in, this, (String)null);
        } 
        catch (UnsupportedEncodingException e)
        {
		    throw new Error(e);
        }
        catch(IOException e)
        {
        	throw new Error(e);
        }
    }    

    /**
     * Creates a HybridReader that uses the given charset. </p>
     * @param  in       An InputStream
     * @param  cs       A charset
     */
    public HybridReader(InputStream in, Charset cs) 
    {
        super(in);
		if (cs == null)
		    throw new NullPointerException("charset");
		sd = StreamDecoder.forInputStreamReader(in, this, cs);
    }

    /**
     * Creates a HybridReader that uses the given charset decoder.  </p>
     * @param  in       An InputStream
     * @param  dec      A charset decoder
     */
    public HybridReader(InputStream in, CharsetDecoder dec) 
    {
        super(in);
		if (dec == null)
		    throw new NullPointerException("charset decoder");
		sd = StreamDecoder.forInputStreamReader(in, this, dec);
    }

    /**
     * Returns the name of the character used by this stream.
     */
    public String getEncoding() 
    {
    	return sd.getEncoding();
    }

    /**
     * Reads a single character.
     * @return The character read, or -1 if the end of the stream has been
     *         reached
     *
     * @exception  IOException  If an I/O error occurs
     */
    @Override
	public int read() throws IOException 
    {
        return sd.read();
    }

    /**
     * Reads characters into a portion of an array.
     * @param      cbuf     Destination buffer
     * @param      offset   Offset at which to start storing characters
     * @param      length   Maximum number of characters to read
     *
     * @return     The number of characters read, or -1 if the end of the 
     *             stream has been reached
     *
     * @exception  IOException  If an I/O error occurs
     */
    @Override
	public int read(char cbuf[], int offset, int length) throws IOException 
    {
    	return sd.read(cbuf, offset, length);
    }

    /**
     * Tells whether this stream is ready to be read.  The reader is
     * ready if its input buffer is not empty, or if bytes are available to be
     * read from the underlying byte stream.
     *
     * @exception  IOException  If an I/O error occurs
     */
    @Override
	public boolean ready() throws IOException 
    {
    	return sd.ready();
    }

    /**
     * Closes the file channel.
     * 
     * @exception IOException If an I/O error occurs
     */
    @Override
	public void close() throws IOException 
    {
    	sd.close();
    }
   
    /***
     * Reads from the current position until the specified delimiter is found or the end of the input stream is reached.
     * 
     * @param delim		
     * 			The byte[] representation of the desired delimiting sequence.
     * 
     * @param skipNL		
     * 			Determines if newlines or carriage returns should be COMPLETELY ignored. Since new lines will also be ignored 
     * 			in possible delimiting sequences, this MUST be set to false if the supplied delimiter representation contains 
     * 			new lines, or bytes with an integer value of 10 or 13 (newline and carriage return value), failure to do so will 
     * 			result in the delimiting sequence never being found.
     * 
     * @param returnDelim  
     * 			Determines if the returned value should include the delimiting sequence.
     * 
     * @return	The data read by the reader.
     * @throws IOException
     */
    public byte[] readTo(byte[] delim, boolean skipNL, boolean returnDelim) throws IOException
    {
    	ByteArrayBuilder bab = new ByteArrayBuilder();
    	int mark = 0;
    	while(ready())
    	{
    		byte i = (byte)read();
    		if(skipNL && (i == 10 || i == 13))
				continue;
    		bab.add(i);
    		if(i == delim[mark])
    		{
    			mark++;
    			if(mark == delim.length)
    				break;
    		}
    		else if(i == delim[0])
    			mark = 1;
    	}
    	return returnDelim ? bab.getArray() : bab.getOffsetArray(0, delim.length);
    }
    
   /**
    * Calls <code>readTo(delim.getBytes(), skipNL, delDelim)</code>  
    * 
    * @param delim
    * @param skipNL
    * @param delDelim
    * @return byte[]
    * @throws IOException
    */
    public byte[] readTo(String delim, boolean skipNL, boolean delDelim) throws IOException
    {
    	return readTo(delim.getBytes(), skipNL, delDelim);
    }
    
    /**
     * Calls <code>readTo(delim.getBytes(), false, true)</code>
     * 
     * @param delim
     * @return byte[]
     * @throws IOException
     */
    public byte[] readTo(String delim) throws IOException
    {
    	return readTo(delim.getBytes(), false, true);
    }
    
    /**
     * Calls <code>readTo(delim, false, true)</code>
     * 
     * @param delim
     * @return byte[]
     * @throws IOException
     */
    public byte[] readTo(byte[] delim) throws IOException
    {
    	return readTo(delim, false, true);
    }
    
    /***
     * 
     * Reads the next instance of TParse represented in the input stream.
     * 
     * @param parseable		
     * 			An instantiated instance of a Parseable class. Instance field values of the passed instance are irrelivant, unless 
     * 			instance variables are used in the implementation of the Parseable interface methods.
     * 
     * @return	The instance of TParseable read from the data source.
     * @throws IOException
     * @throws ParseException 
     */
    public <TParseable> TParseable readParseable(Parseable<TParseable> parseable) throws IOException, ParseException
    {
    	byte[] data = readTo(parseable.getDelimiterValue(), parseable.skipNewLines(), parseable.returnDelimiter());
    	
    	try
    	{
    		return parseable.parse(data);
    	}
    	catch(Exception e)
    	{
    		String name = parseable.getClass().getName();
    		throw new ParseException(
    				"\nThe '"+ name + "' class threw an error in the 'parse(byte[])' method.\n" +
    				"A common cause of this is the existance of any character between the last\n'" +
    				name+"' instance delimiter and the end of the stream. Else,\n" +
    				"the 'parse' method logic does not match with the data it received.\n");    						
    	}
    } 
    
    /***
     * This is a convenience method for easily reading all instaces of TParseable from an InputStream.
     * 
     * @param stream	
     * 				An InputStream from which data can be read. The stream is closed prior to method return.
     * 
     * @param parseable	 
     * 				An instantiated instance of TParseable. Instance field values of the passed instance are irrelivant, unless 
     * 				instance variables are used in TParseable's implementation of the Parseable interface methods.
     * 
     * @return A LinkedList containing all instances of TParseable from the input stream.
     * @throws IOException
     * @throws ParseException 
     */
    public static <TParseable> LinkedList<TParseable> readAllParseable(InputStream stream, Parseable<TParseable> parseable) throws IOException, ParseException
    {
    	HybridReader hr = new HybridReader(stream);
	    	LinkedList<TParseable> list = new LinkedList<TParseable>();
	    	while(hr.ready())
	    		list.add(hr.readParseable(parseable)); 
	    hr.close();
	    return list;
    }
}
